Skip to content

Commit b2d25ef

Browse files
psywalkerLFDanLusnowystinger
authored
Calendar calendar state context set value can not set null value (#6030)
* 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 <dl1644@gmail.com> Co-authored-by: Robert Snow <rsnow@adobe.com>
1 parent 68af923 commit b2d25ef

File tree

5 files changed

+151
-7
lines changed

5 files changed

+151
-7
lines changed

packages/@react-stately/calendar/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ interface CalendarStateBase {
101101

102102
export interface CalendarState extends CalendarStateBase {
103103
/** The currently selected date. */
104-
readonly value: CalendarDate,
104+
readonly value: CalendarDate | null,
105105
/** Sets the currently selected date. */
106-
setValue(value: CalendarDate): void
106+
setValue(value: CalendarDate | null): void
107107
}
108108

109109
export interface RangeCalendarState extends CalendarStateBase {

packages/@react-stately/calendar/src/useCalendarState.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,12 @@ export function useCalendarState<T extends DateValue = DateValue>(props: Calenda
136136
setFocusedDate(date);
137137
}
138138

139-
function setValue(newValue: CalendarDate) {
139+
function setValue(newValue: CalendarDate | null) {
140140
if (!props.isDisabled && !props.isReadOnly) {
141+
if (newValue === null) {
142+
setControlledValue(null);
143+
return;
144+
}
141145
newValue = constrainValue(newValue, minValue, maxValue);
142146
newValue = previousAvailableDate(newValue, startDate, isDateUnavailable);
143147
if (!newValue) {

packages/react-aria-components/docs/Calendar.mdx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,55 @@ function CalendarValue() {
859859
</Calendar>
860860
```
861861

862+
#### Reset value
863+
864+
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`.
865+
866+
```tsx example
867+
import {useContext} from 'react';
868+
import {CalendarStateContext, Button} from 'react-aria-components';
869+
870+
function Footer() {
871+
const state = useContext(CalendarStateContext);
872+
{/*- begin highlight -*/}
873+
const { setValue } = state;
874+
{/*- end highlight -*/}
875+
876+
return (
877+
<div>
878+
<Button
879+
slot={null}
880+
className="reset-button"
881+
onPress={() => {
882+
// reset value
883+
{/*- begin highlight -*/}
884+
setValue(null)
885+
{/*- end highlight -*/}
886+
}}
887+
>
888+
Reset value
889+
</Button>
890+
</div>
891+
);
892+
}
893+
894+
<Calendar>
895+
<header>
896+
<Button slot="previous">◀</Button>
897+
<Heading />
898+
<Button slot="next">▶</Button>
899+
</header>
900+
<CalendarGrid>
901+
{date => <CalendarCell date={date} />}
902+
</CalendarGrid>
903+
{/*- begin highlight -*/}
904+
<Footer />
905+
{/*- end highlight -*/}
906+
</Calendar>
907+
```
908+
909+
910+
862911
### Hooks
863912

864913
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.

packages/react-aria-components/stories/Calendar.stories.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,32 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

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

1616
export default {
1717
title: 'React Aria Components'
1818
};
1919

20+
function Footer() {
21+
const state = useContext(CalendarStateContext);
22+
const setValue = state?.setValue;
23+
24+
return (
25+
<div>
26+
<Button
27+
slot={null}
28+
className="reset-button"
29+
onPress={() => {
30+
// reset value
31+
setValue?.(null);
32+
}}>
33+
Reset value
34+
</Button>
35+
</div>
36+
);
37+
}
38+
2039
export const CalendarExample = () => (
2140
<Calendar style={{width: 220}}>
2241
<div style={{display: 'flex', alignItems: 'center'}}>
@@ -30,6 +49,20 @@ export const CalendarExample = () => (
3049
</Calendar>
3150
);
3251

52+
export const CalendarResetValue = () => (
53+
<Calendar style={{width: 220}}>
54+
<div style={{display: 'flex', alignItems: 'center'}}>
55+
<Button slot="previous">&lt;</Button>
56+
<Heading style={{flex: 1, textAlign: 'center'}} />
57+
<Button slot="next">&gt;</Button>
58+
</div>
59+
<CalendarGrid style={{width: '100%'}}>
60+
{date => <CalendarCell date={date} style={({isSelected, isOutsideMonth}) => ({display: isOutsideMonth ? 'none' : '', textAlign: 'center', cursor: 'default', background: isSelected ? 'blue' : ''})} />}
61+
</CalendarGrid>
62+
<Footer />
63+
</Calendar>
64+
);
65+
3366
export const CalendarMultiMonth = () => (
3467
<Calendar style={{width: 500}} visibleDuration={{months: 2}}>
3568
<div style={{display: 'flex', alignItems: 'center'}}>

packages/react-aria-components/test/Calendar.test.js

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
*/
1212

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

1919
let TestCalendar = ({calendarProps, gridProps, cellProps}) => (
@@ -284,4 +284,62 @@ describe('Calendar', () => {
284284
let headers = getAllByRole('columnheader', {hidden: true});
285285
expect(headers.map(h => h.textContent)).toEqual(['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']);
286286
});
287+
288+
it('should support setting "null" for method setValue', async () => {
289+
290+
const Footer = () => {
291+
const state = useContext(CalendarStateContext);
292+
const {setValue} = state;
293+
294+
return (
295+
<div>
296+
<Button
297+
slot={null}
298+
className="reset-button"
299+
onPress={() => setValue(null)}>
300+
Reset value
301+
</Button>
302+
</div>
303+
);
304+
};
305+
306+
let {getByRole} = render(
307+
<Calendar aria-label="Appointment date" className="grid">
308+
<header>
309+
<Button slot="previous"></Button>
310+
<Heading />
311+
<Button slot="next"></Button>
312+
</header>
313+
<CalendarGrid>
314+
<CalendarGridHeader className="grid-header">
315+
{(day) => (
316+
<CalendarHeaderCell className="header-cell">
317+
{day}
318+
</CalendarHeaderCell>
319+
)}
320+
</CalendarGridHeader>
321+
<CalendarGridBody className="grid-body">
322+
{(date) => <CalendarCell date={date} className={({isSelected}) => isSelected ? 'selected' : ''} />}
323+
</CalendarGridBody>
324+
</CalendarGrid>
325+
<Footer />
326+
</Calendar>
327+
);
328+
let grid = getByRole('application');
329+
expect(grid).toHaveAttribute('class', 'grid');
330+
331+
let cell = within(grid).getAllByRole('button')[7];
332+
expect(cell).toBeInTheDocument();
333+
334+
await user.click(cell);
335+
expect(cell).toHaveAttribute('data-selected', 'true');
336+
expect(cell).toHaveClass('selected');
337+
338+
const resetButton = grid.querySelector('.reset-button');
339+
expect(resetButton).toBeInTheDocument();
340+
341+
await user.click(resetButton);
342+
expect(cell).not.toHaveAttribute('data-selected');
343+
expect(cell).not.toHaveClass('selected');
344+
});
287345
});

0 commit comments

Comments
 (0)