diff --git a/.gitignore b/.gitignore index 4d42a122a..03c250106 100644 --- a/.gitignore +++ b/.gitignore @@ -98,6 +98,7 @@ es tmp .vscode +.history *.iml .idea diff --git a/docs-site/src/components/Examples/config.tsx b/docs-site/src/components/Examples/config.tsx index e593f4d9b..2ba3f4497 100644 --- a/docs-site/src/components/Examples/config.tsx +++ b/docs-site/src/components/Examples/config.tsx @@ -17,6 +17,7 @@ import ConfigureFloatingUI from "../../examples/ts/configureFloatingUI?raw"; import CustomInput from "../../examples/ts/customInput?raw"; import RenderCustomHeader from "../../examples/ts/renderCustomHeader?raw"; import RenderCustomHeaderTwoMonths from "../../examples/ts/renderCustomHeaderTwoMonths?raw"; +import RenderCustomDayName from "../../examples/ts/renderCustomDayName?raw"; import RenderCustomDay from "../../examples/ts/renderCustomDay?raw"; import RenderCustomMonth from "../../examples/ts/renderCustomMonth?raw"; import RenderCustomQuarter from "../../examples/ts/renderCustomQuarter?raw"; @@ -184,6 +185,10 @@ export const EXAMPLE_CONFIG: IExampleConfig[] = [ title: "Custom header with two months displayed", component: RenderCustomHeaderTwoMonths, }, + { + title: "Custom Day Names", + component: RenderCustomDayName, + }, { title: "Custom Day", component: RenderCustomDay, diff --git a/docs-site/src/examples/ts/renderCustomDayName.tsx b/docs-site/src/examples/ts/renderCustomDayName.tsx new file mode 100644 index 000000000..d198e62c7 --- /dev/null +++ b/docs-site/src/examples/ts/renderCustomDayName.tsx @@ -0,0 +1,48 @@ +const RenderCustomDayName = () => { + const [selectedDate, setSelectedDate] = useState(new Date()); + + const renderDayName = ({ + day, + shortName, + fullName, + customDayNameCount, + }: ReactDatePickerCustomDayNameProps): React.ReactNode => { + // Example: Add emoji or custom styling to day names + const dayEmojis: { [key: string]: string } = { + Monday: "🌙", + Tuesday: "🔥", + Wednesday: "🌊", + Thursday: "⚡", + Friday: "🎉", + Saturday: "🌞", + Sunday: "☀️", + }; + + const emoji = dayEmojis[fullName] || ""; + + // Apply different styling based on customDayNameCount when showing multiple months + const style = customDayNameCount > 0 ? { color: "red" } : {}; + + return ( + <> + {fullName} + + + ); + }; + + return ( + + ); +}; + +render(RenderCustomDayName); diff --git a/src/calendar.tsx b/src/calendar.tsx index b5987d86e..228c33f45 100644 --- a/src/calendar.tsx +++ b/src/calendar.tsx @@ -114,6 +114,14 @@ export interface ReactDatePickerCustomHeaderProps { }; } +export interface ReactDatePickerCustomDayNameProps { + day: Date; + shortName: string; + fullName: string; + locale?: Locale; + customDayNameCount: number; +} + type CalendarProps = React.PropsWithChildren< Omit< YearDropdownProps, @@ -195,6 +203,9 @@ type CalendarProps = React.PropsWithChildren< renderCustomHeader?: ( props: ReactDatePickerCustomHeaderProps, ) => React.ReactElement; + renderCustomDayName?: ( + props: ReactDatePickerCustomDayNameProps, + ) => React.ReactNode; onYearMouseEnter?: YearProps["onYearMouseEnter"]; onYearMouseLeave?: YearProps["onYearMouseLeave"]; monthAriaLabelPrefix?: MonthProps["ariaLabelPrefix"]; @@ -477,7 +488,10 @@ export default class Calendar extends Component { ); }; - header = (date: Date = this.state.date): React.ReactElement[] => { + header = ( + date: Date = this.state.date, + customDayNameCount: number = 0, + ): React.ReactElement[] => { // Return empty array if date is invalid if (!isValid(date)) { return []; @@ -507,11 +521,38 @@ export default class Calendar extends Component { [0, 1, 2, 3, 4, 5, 6].map((offset) => { const day = addDays(startOfWeek, offset); const weekDayName = this.formatWeekday(day, this.props.locale); + const fullDayName = formatDate(day, "EEEE", this.props.locale); const weekDayClassName = this.props.weekDayClassName ? this.props.weekDayClassName(day) : undefined; + // Use custom render if provided + if (this.props.renderCustomDayName) { + const customContent = this.props.renderCustomDayName({ + day, + shortName: weekDayName, + fullName: fullDayName, + locale: this.props.locale, + customDayNameCount, + }); + + return ( +
+ {customContent} +
+ ); + } + + // Default render return (
{ disabled ? "react-datepicker__day-name--disabled" : "", )} > - - {formatDate(day, "EEEE", this.props.locale)} - + {fullDayName}
); @@ -871,9 +910,9 @@ export default class Calendar extends Component { ); }; - renderDayNamesHeader = (monthDate: Date) => ( + renderDayNamesHeader = (monthDate: Date, customDayNameCount: number = 0) => (
- {this.header(monthDate)} + {this.header(monthDate, customDayNameCount)}
); @@ -1055,7 +1094,7 @@ export default class Calendar extends Component { selectingDate={this.state.selectingDate} monthShowsDuplicateDaysEnd={monthShowsDuplicateDaysEnd} monthShowsDuplicateDaysStart={monthShowsDuplicateDaysStart} - dayNamesHeader={this.renderDayNamesHeader(monthDate)} + dayNamesHeader={this.renderDayNamesHeader(monthDate, i)} /> , ); diff --git a/src/index.tsx b/src/index.tsx index 6906e112f..c0c44571b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -64,7 +64,10 @@ export { default as CalendarContainer } from "./calendar_container"; export { registerLocale, setDefaultLocale, getDefaultLocale }; -export { ReactDatePickerCustomHeaderProps } from "./calendar"; +export { + ReactDatePickerCustomHeaderProps, + ReactDatePickerCustomDayNameProps, +} from "./calendar"; // Compares dates year+month combinations function hasPreSelectionChanged( diff --git a/src/test/calendar_test.test.tsx b/src/test/calendar_test.test.tsx index c44f5b6b2..db0cdb832 100644 --- a/src/test/calendar_test.test.tsx +++ b/src/test/calendar_test.test.tsx @@ -2864,4 +2864,53 @@ describe("Calendar", () => { ); }); }); + + describe("handleMonthChange with adjustDateOnChange but without setOpen", () => { + it("should call onSelect when adjustDateOnChange is true but setOpen is not provided", () => { + const onSelect = jest.fn(); + const setPreSelection = jest.fn(); + + const { instance } = getCalendar({ + adjustDateOnChange: true, + onSelect, + setPreSelection, + // setOpen is intentionally NOT provided to cover line 442 + selected: new Date("2024-01-15T00:00:00"), + }); + + const targetMonth = new Date("2024-02-01T00:00:00"); + act(() => { + instance?.handleMonthChange(targetMonth); + }); + + expect(onSelect).toHaveBeenCalled(); + expect(setPreSelection).toHaveBeenCalled(); + }); + }); + + describe("header method with invalid date", () => { + it("should return empty array when date is invalid", () => { + const { instance } = getCalendar({ + selected: new Date("2024-01-15T00:00:00"), + }); + + // Call header method with invalid date + const result = instance?.header(new Date("invalid")); + + expect(result).toEqual([]); + }); + + it("should use default date parameter when not provided", () => { + const { instance } = getCalendar({ + selected: new Date("2024-01-15T00:00:00"), + }); + + // Call header method without arguments to cover default parameter + const result = instance?.header(); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result?.length).toBeGreaterThan(0); + }); + }); }); diff --git a/src/test/render_custom_day_name.test.tsx b/src/test/render_custom_day_name.test.tsx new file mode 100644 index 000000000..04c4a681f --- /dev/null +++ b/src/test/render_custom_day_name.test.tsx @@ -0,0 +1,161 @@ +import { render } from "@testing-library/react"; +import React from "react"; + +import DatePicker from "../index"; +import { ReactDatePickerCustomDayNameProps } from "../calendar"; + +describe("renderCustomDayName", () => { + it("should call renderCustomDayName function with correct parameters", () => { + const renderCustomDayName = jest.fn( + ({ shortName }: ReactDatePickerCustomDayNameProps) => ( + {shortName} + ), + ); + + render(); + + // Should be called 7 times (one for each day of the week) + expect(renderCustomDayName).toHaveBeenCalledTimes(7); + + // Check that it's called with correct parameters + const firstCall = renderCustomDayName.mock.calls[0]?.[0]; + expect(firstCall).toBeDefined(); + expect(firstCall).toHaveProperty("day"); + expect(firstCall).toHaveProperty("shortName"); + expect(firstCall).toHaveProperty("fullName"); + expect(firstCall).toHaveProperty("locale"); + expect(firstCall).toHaveProperty("customDayNameCount"); + expect(firstCall?.day).toBeInstanceOf(Date); + expect(typeof firstCall?.shortName).toBe("string"); + expect(typeof firstCall?.fullName).toBe("string"); + expect(typeof firstCall?.customDayNameCount).toBe("number"); + }); + + it("should render custom day names", () => { + const renderCustomDayName = ({ + shortName, + }: ReactDatePickerCustomDayNameProps) => ( + Custom-{shortName} + ); + + const { container } = render( + , + ); + + const customDayNames = container.querySelectorAll(".custom-day-name"); + expect(customDayNames).toHaveLength(7); + expect(customDayNames[0]?.textContent).toContain("Custom-"); + }); + + it("should render default day names when renderCustomDayName is not provided", () => { + const { container } = render(); + + const dayNames = container.querySelectorAll(".react-datepicker__day-name"); + expect(dayNames).toHaveLength(7); + + // Check that default structure is present (sr-only + aria-hidden) + const firstDayName = dayNames[0]; + expect( + firstDayName?.querySelector(".react-datepicker__sr-only"), + ).not.toBeNull(); + expect(firstDayName?.querySelector('[aria-hidden="true"]')).not.toBeNull(); + }); + + it("should use custom day names with accessibility", () => { + const renderCustomDayName = ({ + shortName, + fullName, + }: ReactDatePickerCustomDayNameProps) => ( + <> + {fullName} + + + ); + + const { container } = render( + , + ); + + const dayNames = container.querySelectorAll(".react-datepicker__day-name"); + expect(dayNames).toHaveLength(7); + + // Check that accessibility structure is maintained + dayNames.forEach((dayName) => { + expect( + dayName.querySelector(".react-datepicker__sr-only"), + ).not.toBeNull(); + expect(dayName.querySelector('[aria-hidden="true"]')).not.toBeNull(); + }); + }); + + it("should apply weekDayClassName along with custom day names", () => { + const weekDayClassName = (date: Date) => { + return date.getDay() === 0 || date.getDay() === 6 ? "weekend" : ""; + }; + + const renderCustomDayName = ({ + shortName, + }: ReactDatePickerCustomDayNameProps) => {shortName}; + + const { container } = render( + , + ); + + const weekendDays = container.querySelectorAll( + ".react-datepicker__day-name.weekend", + ); + // Should have 2 weekend days (Saturday and Sunday) + expect(weekendDays.length).toBeGreaterThanOrEqual(2); + }); + + it("should pass locale to renderCustomDayName", () => { + const renderCustomDayName = jest.fn( + ({ shortName }: ReactDatePickerCustomDayNameProps) => ( + {shortName} + ), + ); + + render( + , + ); + + const firstCall = renderCustomDayName.mock.calls[0]?.[0]; + expect(firstCall?.locale).toBe("en-US"); + }); + + it("should pass customDayNameCount when displaying multiple months", () => { + const renderCustomDayName = jest.fn( + ({ shortName }: ReactDatePickerCustomDayNameProps) => ( + {shortName} + ), + ); + + render( + , + ); + + // Should be called 7 times per month, so 21 times for 3 months + expect(renderCustomDayName).toHaveBeenCalledTimes(21); + + // Check that customDayNameCount is different for each month + const firstMonthCall = renderCustomDayName.mock.calls[0]?.[0]; + const secondMonthCall = renderCustomDayName.mock.calls[7]?.[0]; + const thirdMonthCall = renderCustomDayName.mock.calls[14]?.[0]; + + expect(firstMonthCall?.customDayNameCount).toBe(0); + expect(secondMonthCall?.customDayNameCount).toBe(1); + expect(thirdMonthCall?.customDayNameCount).toBe(2); + }); +});