Skip to content

Commit 6bbd234

Browse files
chirokaswingkwongjrgarciadev
authored
Fix DatePicker Time Input (#2845)
* fix(date-picker): set `isCalendarHeaderExpanded` to `false` when DatePicker is closed * fix(date-picker): calendar header controlled state on DatePicker * chore(date-picker): update test * chore(date-picker): remove unnecessary `async` in test * Update packages/components/date-picker/__tests__/date-picker.test.tsx --------- Co-authored-by: WK Wong <wingkwong.code@gmail.com> Co-authored-by: Junior Garcia <jrgarciadev@gmail.com>
1 parent 20ba819 commit 6bbd234

File tree

4 files changed

+158
-4
lines changed

4 files changed

+158
-4
lines changed

.changeset/pretty-crews-build.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nextui-org/date-picker": patch
3+
---
4+
5+
Fix calendar header controlled state on DatePicker.

packages/components/date-picker/__tests__/date-picker.test.tsx

+118
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,124 @@ describe("DatePicker", () => {
459459
});
460460
});
461461

462+
463+
describe("Month and Year Picker", () => {
464+
const onHeaderExpandedChangeSpy = jest.fn();
465+
466+
afterEach(() => {
467+
onHeaderExpandedChangeSpy.mockClear();
468+
});
469+
470+
it("should show the month and year picker (uncontrolled)", () => {
471+
const {getByRole} = render(
472+
<DatePicker
473+
showMonthAndYearPickers
474+
calendarProps={{
475+
onHeaderExpandedChange: onHeaderExpandedChangeSpy,
476+
}}
477+
defaultValue={new CalendarDate(2024, 4, 26)}
478+
label="Date"
479+
/>,
480+
);
481+
482+
const button = getByRole("button");
483+
484+
triggerPress(button);
485+
486+
const dialog = getByRole("dialog");
487+
const header = document.querySelector<HTMLButtonElement>(`button[data-slot="header"]`)!;
488+
489+
expect(dialog).toBeVisible();
490+
expect(onHeaderExpandedChangeSpy).not.toHaveBeenCalled();
491+
492+
triggerPress(header);
493+
494+
const month = getByRole("button", {name: "April"});
495+
const year = getByRole("button", {name: "2024"});
496+
497+
expect(month).toHaveAttribute("data-value", "4");
498+
expect(year).toHaveAttribute("data-value", "2024");
499+
expect(onHeaderExpandedChangeSpy).toHaveBeenCalledTimes(1);
500+
expect(onHeaderExpandedChangeSpy).toHaveBeenCalledWith(true);
501+
502+
triggerPress(button);
503+
504+
expect(dialog).not.toBeInTheDocument();
505+
expect(onHeaderExpandedChangeSpy).toHaveBeenCalledTimes(2);
506+
expect(onHeaderExpandedChangeSpy).toHaveBeenCalledWith(false);
507+
});
508+
509+
it("should show the month and year picker (controlled)", () => {
510+
const {getByRole} = render(
511+
<DatePicker
512+
showMonthAndYearPickers
513+
calendarProps={{
514+
isHeaderExpanded: true,
515+
onHeaderExpandedChange: onHeaderExpandedChangeSpy,
516+
}}
517+
defaultValue={new CalendarDate(2024, 4, 26)}
518+
label="Date"
519+
/>,
520+
);
521+
522+
const button = getByRole("button");
523+
524+
triggerPress(button);
525+
526+
const dialog = getByRole("dialog");
527+
const month = getByRole("button", {name: "April"});
528+
const year = getByRole("button", {name: "2024"});
529+
530+
expect(dialog).toBeVisible();
531+
expect(month).toHaveAttribute("data-value", "4");
532+
expect(year).toHaveAttribute("data-value", "2024");
533+
expect(onHeaderExpandedChangeSpy).not.toHaveBeenCalled();
534+
535+
triggerPress(button);
536+
537+
expect(dialog).not.toBeInTheDocument();
538+
expect(onHeaderExpandedChangeSpy).not.toHaveBeenCalled();
539+
});
540+
541+
it("CalendarBottomContent should render correctly", () => {
542+
const {getByRole, getByTestId} = render(
543+
<DatePicker
544+
showMonthAndYearPickers
545+
CalendarBottomContent={<div data-testid="calendar-bottom-content" />}
546+
label="Date"
547+
/>,
548+
);
549+
550+
const button = getByRole("button");
551+
552+
triggerPress(button);
553+
554+
let dialog = getByRole("dialog");
555+
let calendarBottomContent = getByTestId("calendar-bottom-content");
556+
const header = document.querySelector<HTMLButtonElement>(`button[data-slot="header"]`)!;
557+
558+
expect(dialog).toBeVisible();
559+
expect(calendarBottomContent).toBeVisible();
560+
561+
triggerPress(header);
562+
563+
expect(dialog).toBeVisible();
564+
expect(calendarBottomContent).not.toBeInTheDocument();
565+
566+
triggerPress(button); // close date picker
567+
568+
expect(dialog).not.toBeInTheDocument();
569+
expect(calendarBottomContent).not.toBeInTheDocument();
570+
571+
triggerPress(button);
572+
573+
dialog = getByRole("dialog");
574+
calendarBottomContent = getByTestId("calendar-bottom-content");
575+
576+
expect(dialog).toBeVisible();
577+
expect(calendarBottomContent).toBeVisible();
578+
});
579+
});
462580
it("should close listbox by clicking another datepicker", async () => {
463581
const {getByRole, getAllByRole} = render(
464582
<>

packages/components/date-picker/src/use-date-picker-base.ts

+29-4
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import type {ValueBase} from "@react-types/shared";
99

1010
import {dataAttr} from "@nextui-org/shared-utils";
1111
import {dateInput, DatePickerVariantProps} from "@nextui-org/theme";
12-
import {useState} from "react";
12+
import {useCallback} from "react";
1313
import {HTMLNextUIProps, mapPropsVariants, useProviderContext} from "@nextui-org/system";
1414
import {mergeProps} from "@react-aria/utils";
1515
import {useDOMRef} from "@nextui-org/react-utils";
1616
import {useLocalizedStringFormatter} from "@react-aria/i18n";
17+
import {useControlledState} from "@react-stately/utils";
1718

1819
import intlMessages from "../intl/messages";
1920

@@ -116,8 +117,6 @@ export function useDatePickerBase<T extends DateValue>(originalProps: UseDatePic
116117

117118
const [props, variantProps] = mapPropsVariants(originalProps, dateInput.variantKeys);
118119

119-
const [isCalendarHeaderExpanded, setIsCalendarHeaderExpanded] = useState(false);
120-
121120
const {
122121
as,
123122
ref,
@@ -146,6 +145,24 @@ export function useDatePickerBase<T extends DateValue>(originalProps: UseDatePic
146145
createCalendar,
147146
} = props;
148147

148+
const {
149+
isHeaderExpanded,
150+
isHeaderDefaultExpanded,
151+
onHeaderExpandedChange,
152+
...restUserCalendarProps
153+
} = userCalendarProps;
154+
155+
const handleHeaderExpandedChange = useCallback(
156+
(isExpanded: boolean | undefined) => {
157+
onHeaderExpandedChange?.(isExpanded || false);
158+
},
159+
[onHeaderExpandedChange],
160+
);
161+
162+
const [isCalendarHeaderExpanded, setIsCalendarHeaderExpanded] = useControlledState<
163+
boolean | undefined
164+
>(isHeaderExpanded, isHeaderDefaultExpanded ?? false, handleHeaderExpandedChange);
165+
149166
const domRef = useDOMRef(ref);
150167
const disableAnimation =
151168
originalProps.disableAnimation ?? globalContext?.disableAnimation ?? false;
@@ -194,11 +211,12 @@ export function useDatePickerBase<T extends DateValue>(originalProps: UseDatePic
194211
pageBehavior,
195212
isDateUnavailable,
196213
showMonthAndYearPickers,
214+
isHeaderExpanded: isCalendarHeaderExpanded,
197215
onHeaderExpandedChange: setIsCalendarHeaderExpanded,
198216
color: isDefaultColor ? "primary" : originalProps.color,
199217
disableAnimation,
200218
},
201-
userCalendarProps,
219+
restUserCalendarProps,
202220
),
203221
};
204222

@@ -249,6 +267,12 @@ export function useDatePickerBase<T extends DateValue>(originalProps: UseDatePic
249267
"data-slot": "selector-icon",
250268
};
251269

270+
const onClose = () => {
271+
if (isHeaderExpanded === undefined) {
272+
setIsCalendarHeaderExpanded(false);
273+
}
274+
};
275+
252276
return {
253277
domRef,
254278
endContent,
@@ -272,6 +296,7 @@ export function useDatePickerBase<T extends DateValue>(originalProps: UseDatePic
272296
userTimeInputProps,
273297
selectorButtonProps,
274298
selectorIconProps,
299+
onClose,
275300
};
276301
}
277302

packages/components/date-picker/src/use-date-picker.ts

+6
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,18 @@ export function useDatePicker<T extends DateValue>({
8181
userTimeInputProps,
8282
selectorButtonProps,
8383
selectorIconProps,
84+
onClose,
8485
} = useDatePickerBase({...originalProps, validationBehavior});
8586

8687
let state: DatePickerState = useDatePickerState({
8788
...originalProps,
8889
validationBehavior,
8990
shouldCloseOnSelect: () => !state.hasTime,
91+
onOpenChange: (isOpen) => {
92+
if (!isOpen) {
93+
onClose();
94+
}
95+
},
9096
});
9197

9298
const baseStyles = clsx(classNames?.base, className);

0 commit comments

Comments
 (0)