diff --git a/packages/@react-spectrum/provider/test/Provider.test.tsx b/packages/@react-spectrum/provider/test/Provider.test.tsx index 5788d945cad..85ccbaff1ed 100644 --- a/packages/@react-spectrum/provider/test/Provider.test.tsx +++ b/packages/@react-spectrum/provider/test/Provider.test.tsx @@ -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'; diff --git a/packages/@react-types/calendar/src/index.d.ts b/packages/@react-types/calendar/src/index.d.ts index 6e9f0be69e5..5cdf3791323 100644 --- a/packages/@react-types/calendar/src/index.d.ts +++ b/packages/@react-types/calendar/src/index.d.ts @@ -67,7 +67,7 @@ export interface CalendarPropsBase { export type DateRange = RangeValue | null; export interface CalendarProps extends CalendarPropsBase, ValueBase> {} -export interface RangeCalendarProps extends CalendarPropsBase, ValueBase> { +export interface RangeCalendarProps extends CalendarPropsBase, ValueBase | null> { /** * When combined with `isDateUnavailable`, determines whether non-contiguous ranges, * i.e. ranges containing unavailable dates, may be selected. diff --git a/packages/react-aria-components/src/Calendar.tsx b/packages/react-aria-components/src/Calendar.tsx index a8f1a5a54f2..4966efde564 100644 --- a/packages/react-aria-components/src/Calendar.tsx +++ b/packages/react-aria-components/src/Calendar.tsx @@ -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'; @@ -44,7 +57,7 @@ export interface RangeCalendarRenderProps extends Omit extends Omit, 'errorMessage' | 'validationState'>, RenderProps, SlotProps { +export interface CalendarProps extends Omit, 'errorMessage' | 'validationState'>, RenderProps, SlotProps { /** * The amount of days that will be displayed at once. This affects how pagination works. * @default {months: 1} @@ -52,7 +65,7 @@ export interface CalendarProps extends Omit extends Omit, 'errorMessage' | 'validationState'>, RenderProps, SlotProps { +export interface RangeCalendarProps extends Omit, 'errorMessage' | 'validationState'>, RenderProps, SlotProps { /** * The amount of days that will be displayed at once. This affects how pagination works. * @default {months: 1} diff --git a/packages/react-aria-components/test/RangeCalendar.test.js b/packages/react-aria-components/test/RangeCalendar.test.tsx similarity index 80% rename from packages/react-aria-components/test/RangeCalendar.test.js rename to packages/react-aria-components/test/RangeCalendar.test.tsx index b60b1ae115c..40205915507 100644 --- a/packages/react-aria-components/test/RangeCalendar.test.js +++ b/packages/react-aria-components/test/RangeCalendar.test.tsx @@ -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 = {}}) => (
@@ -29,7 +31,7 @@ let TestCalendar = ({calendarProps, gridProps, cellProps}) => ( ); -let renderCalendar = (calendarProps, gridProps, cellProps) => render(); +let renderCalendar = (calendarProps = {}, gridProps = {}, cellProps = {}) => render(); describe('RangeCalendar', () => { let user; @@ -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 | null>(null); + + return ( + <> + +
+ + + +
+ + {(date) => `${isSelectionStart ? 'start' : ''} ${isSelectionEnd ? 'end' : ''}`} />} + +
+ + + ); + } + let {getByRole} = render( + + ); + + 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');