Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calendar calendar state context set value can not set null value #6030

4 changes: 2 additions & 2 deletions packages/@react-stately/calendar/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ interface CalendarStateBase {

export interface CalendarState extends CalendarStateBase {
/** The currently selected date. */
readonly value: CalendarDate,
readonly value: CalendarDate | null,
/** Sets the currently selected date. */
setValue(value: CalendarDate): void
setValue(value: CalendarDate | null): void
}

export interface RangeCalendarState extends CalendarStateBase {
Expand Down
6 changes: 5 additions & 1 deletion packages/@react-stately/calendar/src/useCalendarState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,12 @@ export function useCalendarState<T extends DateValue = DateValue>(props: Calenda
setFocusedDate(date);
}

function setValue(newValue: CalendarDate) {
function setValue(newValue: CalendarDate | null) {
if (!props.isDisabled && !props.isReadOnly) {
if (newValue === null) {
setControlledValue(null);
return;
}
newValue = constrainValue(newValue, minValue, maxValue);
newValue = previousAvailableDate(newValue, startDate, isDateUnavailable);
if (!newValue) {
Expand Down
49 changes: 49 additions & 0 deletions packages/react-aria-components/docs/Calendar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,55 @@ function CalendarValue() {
</Calendar>
```

#### Reset value

This example shows a custom `Footer` component that can be placed inside the `Calendar.` In this component, we use `CalendarStateContext` to get the `setValue` method from the `CalendarState` object. Next, by clicking on the button, we set the `setValue` to `null` to reset the `Calendar` `value`.

```tsx example
import {useContext} from 'react';
import {CalendarStateContext, Button} from 'react-aria-components';

function Footer() {
const state = useContext(CalendarStateContext);
{/*- begin highlight -*/}
const { setValue } = state;
{/*- end highlight -*/}

return (
<div>
<Button
slot={null}
className="reset-button"
onPress={() => {
// reset value
{/*- begin highlight -*/}
setValue(null)
{/*- end highlight -*/}
}}
>
Reset value
</Button>
</div>
);
}

<Calendar>
<header>
<Button slot="previous">◀</Button>
<Heading />
<Button slot="next">▶</Button>
</header>
<CalendarGrid>
{date => <CalendarCell date={date} />}
</CalendarGrid>
{/*- begin highlight -*/}
<Footer />
{/*- end highlight -*/}
</Calendar>
```



### Hooks

If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See [useCalendar](useCalendar.html) for more details.
Expand Down
37 changes: 35 additions & 2 deletions packages/react-aria-components/stories/Calendar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,32 @@
* governing permissions and limitations under the License.
*/

import {Button, Calendar, CalendarCell, CalendarGrid, Heading, RangeCalendar} from 'react-aria-components';
import React from 'react';
import {Button, Calendar, CalendarCell, CalendarGrid, CalendarStateContext, Heading, RangeCalendar} from 'react-aria-components';
import React, {useContext} from 'react';

export default {
title: 'React Aria Components'
};

function Footer() {
const state = useContext(CalendarStateContext);
const setValue = state?.setValue;

return (
<div>
<Button
slot={null}
className="reset-button"
onPress={() => {
// reset value
setValue?.(null);
}}>
Reset value
</Button>
</div>
);
}

export const CalendarExample = () => (
<Calendar style={{width: 220}}>
<div style={{display: 'flex', alignItems: 'center'}}>
Expand All @@ -30,6 +49,20 @@ export const CalendarExample = () => (
</Calendar>
);

export const CalendarResetValue = () => (
<Calendar style={{width: 220}}>
<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={{width: '100%'}}>
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({display: isOutsideMonth ? 'none' : '', textAlign: 'center', cursor: 'default', background: isSelected ? 'blue' : ''})} />}
</CalendarGrid>
<Footer />
</Calendar>
);

export const CalendarMultiMonth = () => (
<Calendar style={{width: 500}} visibleDuration={{months: 2}}>
<div style={{display: 'flex', alignItems: 'center'}}>
Expand Down
62 changes: 60 additions & 2 deletions packages/react-aria-components/test/Calendar.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
*/

import {act, fireEvent, pointerMap, render, within} from '@react-spectrum/test-utils';
import {Button, Calendar, CalendarCell, CalendarContext, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, Heading} from 'react-aria-components';
import {Button, Calendar, CalendarCell, CalendarContext, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, CalendarStateContext, Heading} from 'react-aria-components';
import {CalendarDate, getLocalTimeZone, startOfMonth, startOfWeek, today} from '@internationalized/date';
import React from 'react';
import React, {useContext} from 'react';
import userEvent from '@testing-library/user-event';

let TestCalendar = ({calendarProps, gridProps, cellProps}) => (
Expand Down Expand Up @@ -284,4 +284,62 @@ describe('Calendar', () => {
let headers = getAllByRole('columnheader', {hidden: true});
expect(headers.map(h => h.textContent)).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']);
});

it('should support setting "null" for method setValue', async () => {

const Footer = () => {
const state = useContext(CalendarStateContext);
const {setValue} = state;

return (
<div>
<Button
slot={null}
className="reset-button"
onPress={() => setValue(null)}>
Reset value
</Button>
</div>
);
};

let {getByRole} = render(
<Calendar aria-label="Appointment date" className="grid">
<header>
<Button slot="previous">◀</Button>
<Heading />
<Button slot="next">▶</Button>
</header>
<CalendarGrid>
<CalendarGridHeader className="grid-header">
{(day) => (
<CalendarHeaderCell className="header-cell">
{day}
</CalendarHeaderCell>
)}
</CalendarGridHeader>
<CalendarGridBody className="grid-body">
{(date) => <CalendarCell date={date} className={({isSelected}) => isSelected ? 'selected' : ''} />}
</CalendarGridBody>
</CalendarGrid>
<Footer />
</Calendar>
);
let grid = getByRole('application');
expect(grid).toHaveAttribute('class', 'grid');

let cell = within(grid).getAllByRole('button')[7];
expect(cell).toBeInTheDocument();

await user.click(cell);
expect(cell).toHaveAttribute('data-selected', 'true');
expect(cell).toHaveClass('selected');

const resetButton = grid.querySelector('.reset-button');
expect(resetButton).toBeInTheDocument();

await user.click(resetButton);
expect(cell).not.toHaveAttribute('data-selected');
expect(cell).not.toHaveClass('selected');
});
});