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>
This commit is contained in:
chirokas 2024-05-25 04:59:17 +08:00 committed by GitHub
parent 20ba81948d
commit 6bbd234aa2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 158 additions and 4 deletions

View File

@ -0,0 +1,5 @@
---
"@nextui-org/date-picker": patch
---
Fix calendar header controlled state on DatePicker.

View File

@ -459,6 +459,124 @@ describe("DatePicker", () => {
});
});
describe("Month and Year Picker", () => {
const onHeaderExpandedChangeSpy = jest.fn();
afterEach(() => {
onHeaderExpandedChangeSpy.mockClear();
});
it("should show the month and year picker (uncontrolled)", () => {
const {getByRole} = render(
<DatePicker
showMonthAndYearPickers
calendarProps={{
onHeaderExpandedChange: onHeaderExpandedChangeSpy,
}}
defaultValue={new CalendarDate(2024, 4, 26)}
label="Date"
/>,
);
const button = getByRole("button");
triggerPress(button);
const dialog = getByRole("dialog");
const header = document.querySelector<HTMLButtonElement>(`button[data-slot="header"]`)!;
expect(dialog).toBeVisible();
expect(onHeaderExpandedChangeSpy).not.toHaveBeenCalled();
triggerPress(header);
const month = getByRole("button", {name: "April"});
const year = getByRole("button", {name: "2024"});
expect(month).toHaveAttribute("data-value", "4");
expect(year).toHaveAttribute("data-value", "2024");
expect(onHeaderExpandedChangeSpy).toHaveBeenCalledTimes(1);
expect(onHeaderExpandedChangeSpy).toHaveBeenCalledWith(true);
triggerPress(button);
expect(dialog).not.toBeInTheDocument();
expect(onHeaderExpandedChangeSpy).toHaveBeenCalledTimes(2);
expect(onHeaderExpandedChangeSpy).toHaveBeenCalledWith(false);
});
it("should show the month and year picker (controlled)", () => {
const {getByRole} = render(
<DatePicker
showMonthAndYearPickers
calendarProps={{
isHeaderExpanded: true,
onHeaderExpandedChange: onHeaderExpandedChangeSpy,
}}
defaultValue={new CalendarDate(2024, 4, 26)}
label="Date"
/>,
);
const button = getByRole("button");
triggerPress(button);
const dialog = getByRole("dialog");
const month = getByRole("button", {name: "April"});
const year = getByRole("button", {name: "2024"});
expect(dialog).toBeVisible();
expect(month).toHaveAttribute("data-value", "4");
expect(year).toHaveAttribute("data-value", "2024");
expect(onHeaderExpandedChangeSpy).not.toHaveBeenCalled();
triggerPress(button);
expect(dialog).not.toBeInTheDocument();
expect(onHeaderExpandedChangeSpy).not.toHaveBeenCalled();
});
it("CalendarBottomContent should render correctly", () => {
const {getByRole, getByTestId} = render(
<DatePicker
showMonthAndYearPickers
CalendarBottomContent={<div data-testid="calendar-bottom-content" />}
label="Date"
/>,
);
const button = getByRole("button");
triggerPress(button);
let dialog = getByRole("dialog");
let calendarBottomContent = getByTestId("calendar-bottom-content");
const header = document.querySelector<HTMLButtonElement>(`button[data-slot="header"]`)!;
expect(dialog).toBeVisible();
expect(calendarBottomContent).toBeVisible();
triggerPress(header);
expect(dialog).toBeVisible();
expect(calendarBottomContent).not.toBeInTheDocument();
triggerPress(button); // close date picker
expect(dialog).not.toBeInTheDocument();
expect(calendarBottomContent).not.toBeInTheDocument();
triggerPress(button);
dialog = getByRole("dialog");
calendarBottomContent = getByTestId("calendar-bottom-content");
expect(dialog).toBeVisible();
expect(calendarBottomContent).toBeVisible();
});
});
it("should close listbox by clicking another datepicker", async () => {
const {getByRole, getAllByRole} = render(
<>

View File

@ -9,11 +9,12 @@ import type {ValueBase} from "@react-types/shared";
import {dataAttr} from "@nextui-org/shared-utils";
import {dateInput, DatePickerVariantProps} from "@nextui-org/theme";
import {useState} from "react";
import {useCallback} from "react";
import {HTMLNextUIProps, mapPropsVariants, useProviderContext} from "@nextui-org/system";
import {mergeProps} from "@react-aria/utils";
import {useDOMRef} from "@nextui-org/react-utils";
import {useLocalizedStringFormatter} from "@react-aria/i18n";
import {useControlledState} from "@react-stately/utils";
import intlMessages from "../intl/messages";
@ -116,8 +117,6 @@ export function useDatePickerBase<T extends DateValue>(originalProps: UseDatePic
const [props, variantProps] = mapPropsVariants(originalProps, dateInput.variantKeys);
const [isCalendarHeaderExpanded, setIsCalendarHeaderExpanded] = useState(false);
const {
as,
ref,
@ -146,6 +145,24 @@ export function useDatePickerBase<T extends DateValue>(originalProps: UseDatePic
createCalendar,
} = props;
const {
isHeaderExpanded,
isHeaderDefaultExpanded,
onHeaderExpandedChange,
...restUserCalendarProps
} = userCalendarProps;
const handleHeaderExpandedChange = useCallback(
(isExpanded: boolean | undefined) => {
onHeaderExpandedChange?.(isExpanded || false);
},
[onHeaderExpandedChange],
);
const [isCalendarHeaderExpanded, setIsCalendarHeaderExpanded] = useControlledState<
boolean | undefined
>(isHeaderExpanded, isHeaderDefaultExpanded ?? false, handleHeaderExpandedChange);
const domRef = useDOMRef(ref);
const disableAnimation =
originalProps.disableAnimation ?? globalContext?.disableAnimation ?? false;
@ -194,11 +211,12 @@ export function useDatePickerBase<T extends DateValue>(originalProps: UseDatePic
pageBehavior,
isDateUnavailable,
showMonthAndYearPickers,
isHeaderExpanded: isCalendarHeaderExpanded,
onHeaderExpandedChange: setIsCalendarHeaderExpanded,
color: isDefaultColor ? "primary" : originalProps.color,
disableAnimation,
},
userCalendarProps,
restUserCalendarProps,
),
};
@ -249,6 +267,12 @@ export function useDatePickerBase<T extends DateValue>(originalProps: UseDatePic
"data-slot": "selector-icon",
};
const onClose = () => {
if (isHeaderExpanded === undefined) {
setIsCalendarHeaderExpanded(false);
}
};
return {
domRef,
endContent,
@ -272,6 +296,7 @@ export function useDatePickerBase<T extends DateValue>(originalProps: UseDatePic
userTimeInputProps,
selectorButtonProps,
selectorIconProps,
onClose,
};
}

View File

@ -81,12 +81,18 @@ export function useDatePicker<T extends DateValue>({
userTimeInputProps,
selectorButtonProps,
selectorIconProps,
onClose,
} = useDatePickerBase({...originalProps, validationBehavior});
let state: DatePickerState = useDatePickerState({
...originalProps,
validationBehavior,
shouldCloseOnSelect: () => !state.hasTime,
onOpenChange: (isOpen) => {
if (!isOpen) {
onClose();
}
},
});
const baseStyles = clsx(classNames?.base, className);