Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ export default {
},
errorMessage: {
control: 'text'
},
selectionAlignment: {
control: 'select',
options: ['start', 'center', 'end']
}
}
} as Meta<typeof Calendar>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ export default {
},
errorMessage: {
control: 'text'
},
selectionAlignment: {
control: 'select',
options: ['start', 'center', 'end']
}
}
} as Meta<typeof RangeCalendar>;
Expand Down
18 changes: 18 additions & 0 deletions packages/@react-spectrum/calendar/test/Calendar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,24 @@ describe('Calendar', () => {
expect(grids[1].contains(cell)).toBe(true);
});

it.each([
{name: 'at the start', alignment: 'start', expected: ['February 2020', 'March 2020', 'April 2020']},
{name: 'in the center', alignment: 'center', expected: ['January 2020', 'February 2020', 'March 2020']},
{name: 'at the end', alignment: 'end', expected: ['December 2019', 'January 2020', 'February 2020']}
])('should align the initial value $name', async ({alignment, expected}) => {
const {getAllByRole} = render(
<Calendar visibleMonths={3} defaultValue={new CalendarDate(2020, 2, 3)} selectionAlignment={alignment} />
);

let grids = getAllByRole('grid');
expect(grids).toHaveLength(3);

expect(grids[0]).toHaveAttribute('aria-label', expected[0]);
expect(grids[1]).toHaveAttribute('aria-label', expected[1]);
expect(grids[2]).toHaveAttribute('aria-label', expected[2]);
});


it('should constrain the visible region depending on the minValue', () => {
let {getAllByRole, getByLabelText} = render(<Calendar value={new CalendarDate(2019, 2, 3)} minValue={new CalendarDate(2019, 2, 1)} visibleMonths={3} />);

Expand Down
17 changes: 17 additions & 0 deletions packages/@react-spectrum/calendar/test/RangeCalendar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,23 @@ describe('RangeCalendar', () => {
expect(cells.every(cell => grids[1].contains(cell))).toBe(true);
});

it.each([
{name: 'at the start', alignment: 'start', expected: ['February 2020', 'March 2020', 'April 2020']},
{name: 'in the center', alignment: 'center', expected: ['January 2020', 'February 2020', 'March 2020']},
{name: 'at the end', alignment: 'end', expected: ['December 2019', 'January 2020', 'February 2020']}
])('should align the initial value $name', async ({alignment, expected}) => {
const {getAllByRole} = render(
<RangeCalendar visibleMonths={3} defaultValue={{start: new CalendarDate(2020, 2, 3), end: new CalendarDate(2020, 2, 10)}} selectionAlignment={alignment} />
);

let grids = getAllByRole('grid');
expect(grids).toHaveLength(3);

expect(grids[0]).toHaveAttribute('aria-label', expected[0]);
expect(grids[1]).toHaveAttribute('aria-label', expected[1]);
expect(grids[2]).toHaveAttribute('aria-label', expected[2]);
});

it('should constrain the visible region depending on the minValue', () => {
let {getAllByRole, getAllByLabelText} = render(<RangeCalendar value={{start: new CalendarDate(2019, 2, 3), end: new CalendarDate(2019, 2, 10)}} minValue={new CalendarDate(2019, 2, 1)} visibleMonths={3} />);

Expand Down
17 changes: 14 additions & 3 deletions packages/@react-stately/calendar/src/useRangeCalendarState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,26 @@ export interface RangeCalendarStateOptions<T extends DateValue = DateValue> exte
* The amount of days that will be displayed at once. This affects how pagination works.
* @default {months: 1}
*/
visibleDuration?: DateDuration
visibleDuration?: DateDuration,
/** Determines how to align the initial selection relative to the visible date range. */
selectionAlignment?: 'start' | 'center' | 'end'
}

/**
* 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.
*/
export function useRangeCalendarState<T extends DateValue = DateValue>(props: RangeCalendarStateOptions<T>): RangeCalendarState {
let {value: valueProp, defaultValue, onChange, createCalendar, locale, visibleDuration = {months: 1}, minValue, maxValue, ...calendarProps} = props;
let {
value: valueProp,
defaultValue,
onChange,
createCalendar,
locale,
visibleDuration = {months: 1},
minValue,
maxValue,
...calendarProps} = props;
let [value, setValue] = useControlledState<RangeValue<T> | null, RangeValue<MappedDateValue<T>>>(
valueProp!,
defaultValue || null!,
Expand Down Expand Up @@ -73,7 +84,7 @@ export function useRangeCalendarState<T extends DateValue = DateValue>(props: Ra
visibleDuration,
minValue: min,
maxValue: max,
selectionAlignment: alignment
selectionAlignment: props.selectionAlignment || alignment
});

let updateAvailableRange = (date) => {
Expand Down
4 changes: 3 additions & 1 deletion packages/@react-types/calendar/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ export interface CalendarPropsBase {
/**
* The day that starts the week.
*/
firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'
firstDayOfWeek?: 'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat',
/** Determines how to align the initial selection relative to the visible date range. */
Copy link
Member

Choose a reason for hiding this comment

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

should changing the selectionAlignment work? or does it only apply on initial render?
maybe something like

Determines the visible months on initial render based on the current selection or current date if there is no selection.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah actually i think it should only apply on initial render. i'll update the description

selectionAlignment?: 'start' | 'center' | 'end'
}

export type DateRange = RangeValue<DateValue> | null;
Expand Down
53 changes: 48 additions & 5 deletions packages/react-aria-components/stories/Calendar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default {
} as Meta<typeof Calendar>;

export type CalendarStory = StoryObj<typeof Calendar>;

export type RangeCalendarStory = StoryObj<typeof RangeCalendar>;

function Footer() {
const state = useContext(CalendarStateContext);
Expand Down Expand Up @@ -74,8 +74,8 @@ export const CalendarResetValue: CalendarStory = {
};

export const CalendarMultiMonth: CalendarStory = {
render: () => (
<Calendar style={{width: 500}} visibleDuration={{months: 2}}>
render: (args) => (
<Calendar style={{width: 500}} visibleDuration={{months: 3}} {...args}>
<div style={{display: 'flex', alignItems: 'center'}}>
<Button slot="previous">&lt;</Button>
<Heading style={{flex: 1, textAlign: 'center'}} />
Expand All @@ -88,12 +88,24 @@ export const CalendarMultiMonth: CalendarStory = {
<CalendarGrid style={{flex: 1}} offset={{months: 1}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({opacity: isOutsideMonth ? '0.5' : '', textAlign: 'center', cursor: 'default', background: isSelected && !isOutsideMonth ? 'blue' : ''})} />}
</CalendarGrid>
<CalendarGrid style={{flex: 1}} offset={{months: 2}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({opacity: isOutsideMonth ? '0.5' : '', textAlign: 'center', cursor: 'default', background: isSelected && !isOutsideMonth ? 'blue' : ''})} />}
</CalendarGrid>
</div>
</Calendar>
)
),
args: {
selectionAlignment: 'center'
},
argTypes: {
selectionAlignment: {
control: 'select',
options: ['start', 'center', 'end']
}
}
};

export const RangeCalendarExample: CalendarStory = {
export const RangeCalendarExample: RangeCalendarStory = {
render: () => (
<RangeCalendar style={{width: 220}}>
<div style={{display: 'flex', alignItems: 'center'}}>
Expand All @@ -107,3 +119,34 @@ export const RangeCalendarExample: CalendarStory = {
</RangeCalendar>
)
};


export const RangeCalendarMultiMonthExample: RangeCalendarStory = {
render: (args) => (
<RangeCalendar style={{width: 500}} visibleDuration={{months: 3}} {...args} >
<div style={{display: 'flex', alignItems: 'center'}}>
<Button slot="previous">&lt;</Button>
<Heading style={{flex: 1, textAlign: 'center'}} />
<Button slot="next">&gt;</Button>
</div>
<CalendarGrid style={{flex: 1}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({display: isOutsideMonth ? 'none' : '', textAlign: 'center', cursor: 'default', background: isSelected ? 'blue' : ''})} />}
</CalendarGrid>
<CalendarGrid style={{flex: 1}} offset={{months: 1}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({display: isOutsideMonth ? 'none' : '', textAlign: 'center', cursor: 'default', background: isSelected ? 'blue' : ''})} />}
</CalendarGrid>
<CalendarGrid style={{flex: 1}} offset={{months: 2}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({display: isOutsideMonth ? 'none' : '', textAlign: 'center', cursor: 'default', background: isSelected ? 'blue' : ''})} />}
</CalendarGrid>
</RangeCalendar>
),
args: {
selectionAlignment: 'center'
},
argTypes: {
selectionAlignment: {
control: 'select',
options: ['start', 'center', 'end']
}
}
};
35 changes: 35 additions & 0 deletions packages/react-aria-components/test/Calendar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,41 @@ describe('Calendar', () => {
expect(grids[1]).toHaveAttribute('aria-label', 'Appointment date, ' + formatter.format(today(getLocalTimeZone()).add({months: 1}).toDate(getLocalTimeZone())));
});


it.each([
{name: 'at the start', alignment: 'start', expected: ['February 2020', 'March 2020', 'April 2020']},
{name: 'in the center', alignment: 'center', expected: ['January 2020', 'February 2020', 'March 2020']},
{name: 'at the end', alignment: 'end', expected: ['December 2019', 'January 2020', 'February 2020']}
])('should align the initial value $name', async ({alignment, expected}) => {
const {getAllByRole} = render(
<Calendar visibleDuration={{months: 3}} defaultValue={new CalendarDate(2020, 2, 3)} selectionAlignment={alignment}>
<header>
<Button slot="previous">◀</Button>
<Heading />
<Button slot="next">▶</Button>
</header>
<div style={{display: 'flex', gap: 30}}>
<CalendarGrid>
{date => <CalendarCell date={date} />}
</CalendarGrid>
<CalendarGrid offset={{months: 1}}>
{date => <CalendarCell date={date} />}
</CalendarGrid>
<CalendarGrid offset={{months: 2}}>
{date => <CalendarCell date={date} />}
</CalendarGrid>
</div>
</Calendar>
);

let grids = getAllByRole('grid');
expect(grids).toHaveLength(3);

expect(grids[0]).toHaveAttribute('aria-label', expected[0]);
expect(grids[1]).toHaveAttribute('aria-label', expected[1]);
expect(grids[2]).toHaveAttribute('aria-label', expected[2]);
});

it('should support hover', async () => {
let hoverStartSpy = jest.fn();
let hoverChangeSpy = jest.fn();
Expand Down
35 changes: 35 additions & 0 deletions packages/react-aria-components/test/RangeCalendar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,41 @@ describe('RangeCalendar', () => {
expect(grids[1]).toHaveAttribute('aria-label', 'Trip dates, ' + formatter.format(today(getLocalTimeZone()).add({months: 1}).toDate(getLocalTimeZone())));
});

it.each([
{name: 'at the start', alignment: 'start', expected: ['February 2020', 'March 2020', 'April 2020']},
{name: 'in the center', alignment: 'center', expected: ['January 2020', 'February 2020', 'March 2020']},
{name: 'at the end', alignment: 'end', expected: ['December 2019', 'January 2020', 'February 2020']}
])('should align the initial value $name', async ({alignment, expected}) => {
const {getAllByRole} = render(
<RangeCalendar visibleDuration={{months: 3}} defaultValue={{start: new CalendarDate(2020, 2, 3), end: new CalendarDate(2020, 2, 10)}} selectionAlignment={alignment as 'start' | 'center' | 'end'}>
<header>
<Button slot="previous">◀</Button>
<Heading />
<Button slot="next">▶</Button>
</header>
<div style={{display: 'flex', gap: 30}}>
<CalendarGrid>
{date => <CalendarCell date={date} />}
</CalendarGrid>
<CalendarGrid offset={{months: 1}}>
{date => <CalendarCell date={date} />}
</CalendarGrid>
<CalendarGrid offset={{months: 2}}>
{date => <CalendarCell date={date} />}
</CalendarGrid>
</div>
</RangeCalendar>
);

let grids = getAllByRole('grid');
expect(grids).toHaveLength(3);

expect(grids[0]).toHaveAttribute('aria-label', expected[0]);
expect(grids[1]).toHaveAttribute('aria-label', expected[1]);
expect(grids[2]).toHaveAttribute('aria-label', expected[2]);
});


it('should support hover', async () => {
let {getByRole} = renderCalendar({}, {}, {className: ({isHovered}) => isHovered ? 'hover' : ''});
let grid = getByRole('grid');
Expand Down