Skip to content

Commit f629719

Browse files
authored
Fix clearing date picker by setting value to null (#4312)
1 parent 17ea50d commit f629719

File tree

5 files changed

+150
-1
lines changed

5 files changed

+150
-1
lines changed

packages/@react-spectrum/datepicker/stories/DateRangePicker.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ function ControlledExample(props) {
308308

309309
return (
310310
<Flex direction="column" alignItems="center" gap="size-150">
311-
<DateRangePicker label="Controlled" {...props} value={value} onChange={chain(setValue, action('onChange'))} />
311+
<DateRangePicker label="Controlled" {...props} granularity="minute" value={value} onChange={chain(setValue, action('onChange'))} />
312312
<ActionButton onPress={() => setValue({start: new CalendarDate(2020, 2, 3), end: new CalendarDate(2020, 5, 4)})}>Change value</ActionButton>
313313
<ActionButton onPress={() => setValue(null)}>Clear</ActionButton>
314314
</Flex>

packages/@react-spectrum/datepicker/test/DatePicker.test.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,74 @@ describe('DatePicker', function () {
620620
expect(dialog).not.toBeInTheDocument();
621621
expect(onChange).not.toHaveBeenCalled();
622622
});
623+
624+
it('should clear date and time when controlled value is set to null', function () {
625+
function ControlledDatePicker() {
626+
let [value, setValue] = React.useState(null);
627+
return (<>
628+
<DatePicker label="Date" granularity="minute" value={value} onChange={setValue} />
629+
<button onClick={() => setValue(null)}>Clear</button>
630+
</>);
631+
}
632+
633+
let {getAllByRole, getAllByLabelText} = render(
634+
<Provider theme={theme}>
635+
<ControlledDatePicker />
636+
</Provider>
637+
);
638+
639+
let combobox = getAllByRole('group')[0];
640+
let formatter = new Intl.DateTimeFormat('en-US', {year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric'});
641+
expectPlaceholder(combobox, 'mm/dd/yyyy, ––:–– AM');
642+
643+
let button = getAllByRole('button')[0];
644+
triggerPress(button);
645+
646+
let cells = getAllByRole('gridcell');
647+
let timeField = getAllByLabelText('Time')[0];
648+
let todayCell = cells.find(cell => cell.firstChild.getAttribute('aria-label')?.startsWith('Today'));
649+
triggerPress(todayCell.firstChild);
650+
651+
expect(todayCell).toHaveAttribute('aria-selected', 'true');
652+
653+
let hour = within(timeField).getByLabelText('hour');
654+
act(() => hour.focus());
655+
fireEvent.keyDown(hour, {key: 'ArrowUp'});
656+
fireEvent.keyUp(hour, {key: 'ArrowUp'});
657+
expect(hour).toHaveAttribute('aria-valuetext', '12 AM');
658+
659+
fireEvent.keyDown(hour, {key: 'ArrowRight'});
660+
fireEvent.keyUp(hour, {key: 'ArrowRight'});
661+
expect(document.activeElement).toHaveAttribute('aria-label', 'minute');
662+
fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'});
663+
fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'});
664+
expect(document.activeElement).toHaveAttribute('aria-valuetext', '00');
665+
666+
fireEvent.keyDown(hour, {key: 'ArrowRight'});
667+
fireEvent.keyUp(hour, {key: 'ArrowRight'});
668+
expect(document.activeElement).toHaveAttribute('aria-label', 'AM/PM');
669+
fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'});
670+
fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'});
671+
expect(document.activeElement).toHaveAttribute('aria-valuetext', 'AM');
672+
673+
userEvent.click(document.body);
674+
act(() => jest.runAllTimers());
675+
676+
let value = toCalendarDateTime(today(getLocalTimeZone()));
677+
expectPlaceholder(combobox, formatter.format(value.toDate(getLocalTimeZone())));
678+
679+
let clear = getAllByRole('button')[1];
680+
triggerPress(clear);
681+
expectPlaceholder(combobox, 'mm/dd/yyyy, ––:–– AM');
682+
683+
triggerPress(button);
684+
cells = getAllByRole('gridcell');
685+
let selected = cells.find(cell => cell.getAttribute('aria-selected') === 'true');
686+
expect(selected).toBeUndefined();
687+
688+
timeField = getAllByLabelText('Time')[0];
689+
expectPlaceholder(timeField, '––:–– AM');
690+
});
623691
});
624692

625693
describe('labeling', function () {

packages/@react-spectrum/datepicker/test/DateRangePicker.test.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,83 @@ describe('DateRangePicker', function () {
712712
expect(dialog).not.toBeInTheDocument();
713713
expect(onChange).not.toHaveBeenCalled();
714714
});
715+
716+
it('should clear date and time when controlled value is set to null', function () {
717+
function ControlledDateRangePicker() {
718+
let [value, setValue] = React.useState(null);
719+
return (<>
720+
<DateRangePicker label="Date" granularity="minute" value={value} onChange={setValue} />
721+
<button onClick={() => setValue(null)}>Clear</button>
722+
</>);
723+
}
724+
725+
let {getAllByRole, getAllByLabelText, getByTestId} = render(
726+
<Provider theme={theme}>
727+
<ControlledDateRangePicker />
728+
</Provider>
729+
);
730+
731+
let formatter = new Intl.DateTimeFormat('en-US', {year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric'});
732+
let startDate = getByTestId('start-date');
733+
let endDate = getByTestId('end-date');
734+
expectPlaceholder(startDate, 'mm/dd/yyyy, ––:–– AM');
735+
expectPlaceholder(endDate, 'mm/dd/yyyy, ––:–– AM');
736+
737+
let button = getAllByRole('button')[0];
738+
triggerPress(button);
739+
740+
let cells = getAllByRole('gridcell');
741+
let startTimeField = getAllByLabelText('Start time')[0];
742+
let endTimeField = getAllByLabelText('End time')[0];
743+
744+
let enabledCells = cells.filter(cell => !cell.hasAttribute('aria-disabled'));
745+
triggerPress(enabledCells[0].firstChild);
746+
triggerPress(enabledCells[1].firstChild);
747+
748+
for (let timeField of [startTimeField, endTimeField]) {
749+
let hour = within(timeField).getByLabelText('hour');
750+
act(() => hour.focus());
751+
fireEvent.keyDown(hour, {key: 'ArrowUp'});
752+
fireEvent.keyUp(hour, {key: 'ArrowUp'});
753+
expect(hour).toHaveAttribute('aria-valuetext', '12 AM');
754+
755+
fireEvent.keyDown(hour, {key: 'ArrowRight'});
756+
fireEvent.keyUp(hour, {key: 'ArrowRight'});
757+
expect(document.activeElement).toHaveAttribute('aria-label', 'minute');
758+
fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'});
759+
fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'});
760+
expect(document.activeElement).toHaveAttribute('aria-valuetext', '00');
761+
762+
fireEvent.keyDown(hour, {key: 'ArrowRight'});
763+
fireEvent.keyUp(hour, {key: 'ArrowRight'});
764+
expect(document.activeElement).toHaveAttribute('aria-label', 'AM/PM');
765+
fireEvent.keyDown(document.activeElement, {key: 'ArrowUp'});
766+
fireEvent.keyUp(document.activeElement, {key: 'ArrowUp'});
767+
}
768+
769+
userEvent.click(document.body);
770+
act(() => jest.runAllTimers());
771+
772+
let startValue = toCalendarDateTime(today(getLocalTimeZone())).set({day: 1});
773+
let endValue = toCalendarDateTime(today(getLocalTimeZone())).set({day: 2});
774+
expect(getTextValue(startDate)).toBe(formatter.format(startValue.toDate(getLocalTimeZone())));
775+
expect(getTextValue(endDate)).toBe(formatter.format(endValue.toDate(getLocalTimeZone())));
776+
777+
let clear = getAllByRole('button')[1];
778+
triggerPress(clear);
779+
expectPlaceholder(startDate, 'mm/dd/yyyy, ––:–– AM');
780+
expectPlaceholder(endDate, 'mm/dd/yyyy, ––:–– AM');
781+
782+
triggerPress(button);
783+
cells = getAllByRole('gridcell');
784+
let selected = cells.find(cell => cell.getAttribute('aria-selected') === 'true');
785+
expect(selected).toBeUndefined();
786+
787+
startTimeField = getAllByLabelText('Start time')[0];
788+
endTimeField = getAllByLabelText('End time')[0];
789+
expectPlaceholder(startTimeField, '––:–– AM');
790+
expectPlaceholder(endTimeField, '––:–– AM');
791+
});
715792
});
716793

717794
describe('labeling', function () {

packages/@react-stately/datepicker/src/useDatePickerState.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ export function useDatePickerState<T extends DateValue = DateValue>(props: DateP
9191

9292
let commitValue = (date: DateValue, time: TimeValue) => {
9393
setValue('timeZone' in time ? time.set(toCalendarDate(date)) : toCalendarDateTime(date, time));
94+
setSelectedDate(null);
95+
setSelectedTime(null);
9496
};
9597

9698
// Intercept setValue to make sure the Time section is not changed by date selection in Calendar

packages/@react-stately/datepicker/src/useDateRangePickerState.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,8 @@ export function useDateRangePickerState<T extends DateValue = DateValue>(props:
113113
start: 'timeZone' in timeRange.start ? timeRange.start.set(toCalendarDate(dateRange.start)) : toCalendarDateTime(dateRange.start, timeRange.start),
114114
end: 'timeZone' in timeRange.end ? timeRange.end.set(toCalendarDate(dateRange.end)) : toCalendarDateTime(dateRange.end, timeRange.end)
115115
});
116+
setSelectedDateRange(null);
117+
setSelectedTimeRange(null);
116118
};
117119

118120
// Intercept setValue to make sure the Time section is not changed by date selection in Calendar

0 commit comments

Comments
 (0)