Skip to content

Commit

Permalink
Calendar calendar state context set value can not set null value (#6030)
Browse files Browse the repository at this point in the history
* Fixed the inability to set the null for setValue from CalendarStateContext

* added example in docs of calendar

* added example in storybook

* added test for setting "null" for method setValue

* fixed eslint bug

* fixed eslint bug

* fixed ts error

* review comments

---------

Co-authored-by: Daniel Lu <[email protected]>
Co-authored-by: Robert Snow <[email protected]>
  • Loading branch information
3 people authored Apr 8, 2024
1 parent 68af923 commit b2d25ef
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 7 deletions.
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');
});
});

1 comment on commit b2d25ef

@rspbot
Copy link

@rspbot rspbot commented on b2d25ef Apr 8, 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.