Skip to content

Commit

Permalink
Fix type regression in calendar (#6790)
Browse files Browse the repository at this point in the history
* Fix type regression in RangeCalendar
  • Loading branch information
snowystinger authored Aug 7, 2024
1 parent 1af59d1 commit dcfa1b0
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 6 deletions.
1 change: 1 addition & 0 deletions packages/@react-spectrum/provider/test/Provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

// needs to be imported first
// eslint-disable-next-line
import MatchMediaMock from 'jest-matchmedia-mock';
// eslint-disable-next-line rsp-rules/sort-imports
import {act, fireEvent, pointerMap, render} from '@react-spectrum/test-utils-internal';
Expand Down
2 changes: 1 addition & 1 deletion packages/@react-types/calendar/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export interface CalendarPropsBase {

export type DateRange = RangeValue<DateValue> | null;
export interface CalendarProps<T extends DateValue | null> extends CalendarPropsBase, ValueBase<T | null, MappedDateValue<T>> {}
export interface RangeCalendarProps<T extends DateValue | null> extends CalendarPropsBase, ValueBase<RangeValue<T>> {
export interface RangeCalendarProps<T extends DateValue | null> extends CalendarPropsBase, ValueBase<RangeValue<T> | null> {
/**
* When combined with `isDateUnavailable`, determines whether non-contiguous ranges,
* i.e. ranges containing unavailable dates, may be selected.
Expand Down
19 changes: 16 additions & 3 deletions packages/react-aria-components/src/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,20 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import {CalendarProps as BaseCalendarProps, RangeCalendarProps as BaseRangeCalendarProps, DateValue, mergeProps, useCalendar, useCalendarCell, useCalendarGrid, useFocusRing, useHover, useLocale, useRangeCalendar, VisuallyHidden} from 'react-aria';
import {
AriaCalendarProps,
AriaRangeCalendarProps,
DateValue,
mergeProps,
useCalendar,
useCalendarCell,
useCalendarGrid,
useFocusRing,
useHover,
useLocale,
useRangeCalendar,
VisuallyHidden
} from 'react-aria';
import {ButtonContext} from './Button';
import {CalendarDate, createCalendar, DateDuration, endOfMonth, getWeeksInMonth, isSameDay, isSameMonth} from '@internationalized/date';
import {CalendarState, RangeCalendarState, useCalendarState, useRangeCalendarState} from 'react-stately';
Expand Down Expand Up @@ -44,15 +57,15 @@ export interface RangeCalendarRenderProps extends Omit<CalendarRenderProps, 'sta
state: RangeCalendarState
}

export interface CalendarProps<T extends DateValue> extends Omit<BaseCalendarProps<T>, 'errorMessage' | 'validationState'>, RenderProps<CalendarRenderProps>, SlotProps {
export interface CalendarProps<T extends DateValue> extends Omit<AriaCalendarProps<T>, 'errorMessage' | 'validationState'>, RenderProps<CalendarRenderProps>, SlotProps {
/**
* The amount of days that will be displayed at once. This affects how pagination works.
* @default {months: 1}
*/
visibleDuration?: DateDuration
}

export interface RangeCalendarProps<T extends DateValue> extends Omit<BaseRangeCalendarProps<T>, 'errorMessage' | 'validationState'>, RenderProps<RangeCalendarRenderProps>, SlotProps {
export interface RangeCalendarProps<T extends DateValue> extends Omit<AriaRangeCalendarProps<T>, 'errorMessage' | 'validationState'>, RenderProps<RangeCalendarRenderProps>, SlotProps {
/**
* The amount of days that will be displayed at once. This affects how pagination works.
* @default {months: 1}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
import {act, fireEvent, pointerMap, render, within} from '@react-spectrum/test-utils-internal';
import {Button, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, Heading, RangeCalendar, RangeCalendarContext} from 'react-aria-components';
import {CalendarDate, getLocalTimeZone, startOfMonth, startOfWeek, today} from '@internationalized/date';
import {DateValue} from '@react-types/calendar';
import {RangeValue} from '@react-types/shared';
import React from 'react';
import userEvent from '@testing-library/user-event';

let TestCalendar = ({calendarProps, gridProps, cellProps}) => (
let TestCalendar = ({calendarProps = {}, gridProps = {}, cellProps = {}}) => (
<RangeCalendar aria-label="Trip dates" {...calendarProps}>
<header>
<Button slot="previous"></Button>
Expand All @@ -29,7 +31,7 @@ let TestCalendar = ({calendarProps, gridProps, cellProps}) => (
</RangeCalendar>
);

let renderCalendar = (calendarProps, gridProps, cellProps) => render(<TestCalendar {...{calendarProps, gridProps, cellProps}} />);
let renderCalendar = (calendarProps = {}, gridProps = {}, cellProps = {}) => render(<TestCalendar {...{calendarProps, gridProps, cellProps}} />);

describe('RangeCalendar', () => {
let user;
Expand Down Expand Up @@ -282,6 +284,72 @@ describe('RangeCalendar', () => {
expect(cells[8]).not.toHaveClass('end');
});

it('should support controlled selected range states', async () => {
function ControlledCalendar() {
let [value, setValue] = React.useState<RangeValue<DateValue> | null>(null);

return (
<>
<RangeCalendar aria-label="Trip dates" value={value} onChange={setValue}>
<header>
<Button slot="previous"></Button>
<Heading />
<Button slot="next"></Button>
</header>
<CalendarGrid>
{(date) => <CalendarCell date={date} className={({isSelectionStart, isSelectionEnd}) => `${isSelectionStart ? 'start' : ''} ${isSelectionEnd ? 'end' : ''}`} />}
</CalendarGrid>
</RangeCalendar>
<Button onPress={() => setValue(null)}>Reset</Button>
</>
);
}
let {getByRole} = render(
<ControlledCalendar />
);

let resetBtn = getByRole('button', {name: 'Reset'});
let grid = getByRole('grid');
let cells = within(grid).getAllByRole('button');

expect(cells[7]).not.toHaveAttribute('data-selection-start');
expect(cells[7]).not.toHaveClass('start');
expect(cells[7]).not.toHaveClass('end');

await user.click(cells[7]);
expect(cells[7]).toHaveAttribute('data-selection-start', 'true');
expect(cells[7]).toHaveClass('start');
expect(cells[7]).toHaveAttribute('data-selection-end', 'true');
expect(cells[7]).toHaveClass('end');

expect(cells[8]).not.toHaveAttribute('data-selection-start', 'true');
expect(cells[8]).not.toHaveClass('start');
expect(cells[8]).not.toHaveAttribute('data-selection-end', 'true');
expect(cells[8]).not.toHaveClass('end');

await user.click(cells[10]);
expect(cells[7]).toHaveAttribute('data-selection-start', 'true');
expect(cells[7]).toHaveClass('start');
expect(cells[7]).not.toHaveAttribute('data-selection-end', 'true');
expect(cells[7]).not.toHaveClass('end');
expect(cells[10]).toHaveAttribute('data-selection-end', 'true');
expect(cells[10]).toHaveClass('end');

expect(cells[8]).not.toHaveAttribute('data-selection-start', 'true');
expect(cells[8]).not.toHaveClass('start');
expect(cells[8]).not.toHaveAttribute('data-selection-end', 'true');
expect(cells[8]).not.toHaveClass('end');

await user.click(resetBtn);

expect(cells[7]).not.toHaveAttribute('data-selection-start');
expect(cells[7]).not.toHaveClass('start');
expect(cells[7]).not.toHaveClass('end');
expect(cells[10]).not.toHaveAttribute('data-selection-end');
expect(cells[10]).not.toHaveClass('end');

});

it('should support unavailable state', () => {
let {getByRole} = renderCalendar({isDateUnavailable: () => true}, {}, {className: ({isUnavailable}) => isUnavailable ? 'unavailable' : ''});
let grid = getByRole('grid');
Expand Down

1 comment on commit dcfa1b0

@rspbot
Copy link

@rspbot rspbot commented on dcfa1b0 Aug 7, 2024

Choose a reason for hiding this comment

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

Please sign in to comment.